#define OLC_PGE_APPLICATION
#include <olcPixelGameEngine.hpp>

#define OLC_PGEX_TRANSFORMEDVIEW
#include <olcPGEX_TransformedView.hpp>

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include "dw_net.hpp"
#include "digital_world.hpp"
using namespace dw;

#include <list>
#include <iostream>
using namespace std;

enum class DWMsgType : uint32_t
{
	ClientAccept,
	PingServer,

	CreateAgent,

	POST_WorldState,
	POST_AgentState,
	POST_PathState,
	POST_MapState,

	GET_WorldState,
	GET_AgentState,
	GET_PathState,
	GET_MapState
};

uint32_t dw::net::Scramble(uint32_t in)
{
	return in;
}

class DigitalWorldUI :
	public olc::PixelGameEngine,
	public dw::net::client_interface<DWMsgType>
{
public:
	DigitalWorldUI()
	{
		sAppName = "Digital World UI";
	}

private:
	olc::TileTransformedView tv;

	World world;

	bool bFollowObject = false;
	bool bWaitingForConnection = true;

	ID FocusedAgentID = 0;
	shared_ptr<Agent> pFocusedAgent;

	uint32_t n_frame = 0;

public:
	bool OnUserCreate() override
	{
		// Create "Tiled World", where each tile is 32x32 screen pixels. Coordinates
		// for drawing will exist in unit-tile space from now on...
		tv = olc::TileTransformedView({ ScreenWidth(), ScreenHeight() }, { 32, 32 });

		if (Connect("43.142.137.93", 60000))
		{
			return true;
		}

		std::cerr << "Can't connect to server!" << std::endl;
		return false;
	}

	bool OnUserUpdate(float fElapsedTime) override
	{
		if (IsConnected())
		{
			if (!Incomming().empty())
			{
				dw::net::message<DWMsgType> msg = Incomming().pop_front().msg;

				switch (msg.header.type)
				{
				case DWMsgType::ClientAccept:
				{
					dw::net::message<DWMsgType> m(DWMsgType::GET_MapState);
					Send(m);
				}
				break;

				case DWMsgType::POST_MapState:
				{
					std::string encoded(msg.body.begin(), msg.body.end());
					world.decodeMap(encoded);

					dw::net::message<DWMsgType> m(DWMsgType::GET_WorldState);
					Send(m);
				}
				break;

				case DWMsgType::POST_WorldState:
				{
					std::string encoded(msg.body.begin(), msg.body.end());
					cout << encoded << endl;
					world.decode(encoded);
					if (!FocusedAgentID) {
						FocusedAgentID = (*world.agent_begin()).first;
						pFocusedAgent = (*world.agent_begin()).second;
					}
					cout << world << endl;
					bWaitingForConnection = false;
				}
				break;
				default: break;
				}
			}
			else {
				if (!bWaitingForConnection) {
					if (n_frame) {
						--n_frame;
					}
					else
					{
						dw::net::message m(DWMsgType::GET_WorldState);
						Send(m);
						n_frame = 50;
						world.Update();
					}
				}
			}
		}

		if (bWaitingForConnection)
		{
			Clear(olc::DARK_BLUE);
			DrawString({ 10,10 }, "Waiting to Connect...", olc::WHITE);
			return true;
		}


		if (GetKey(olc::Key::SPACE).bReleased) bFollowObject = !bFollowObject;

		// Determine which one is the focused agent
		if (GetKey(olc::Key::SHIFT).bReleased)
		{
			bool flag = false;
			for (auto iter = world.agent_begin(); iter != world.agent_end(); ++iter) {
				ID id = (*iter).first;

				if (flag) {
					FocusedAgentID = id;
					pFocusedAgent = (*iter).second;
					flag = false;
					break;
				}

				if (FocusedAgentID == id) {
					flag = true;
				}
			}

			if (flag) {
				FocusedAgentID = (*world.agent_begin()).first;
				pFocusedAgent = (*world.agent_begin()).second;
			}
		}

		// Clear World
		Clear(olc::VERY_DARK_BLUE);

		if (bFollowObject)
		{
			tv.SetWorldOffset(pFocusedAgent->getPosition() - tv.ScaleToWorld(olc::vf2d(ScreenWidth() / 2.0f, ScreenHeight() / 2.0f)));
			DrawString({ 10,10 }, "Following Object");
		}

		// Handle Pan & Zoom
		if (GetMouse(2).bPressed) tv.StartPan(GetMousePos());
		if (GetMouse(2).bHeld) tv.UpdatePan(GetMousePos());
		if (GetMouse(2).bReleased) tv.EndPan(GetMousePos());
		if (GetMouseWheel() > 0) tv.ZoomAtScreenPos(2.0f, GetMousePos());
		if (GetMouseWheel() < 0) tv.ZoomAtScreenPos(0.5f, GetMousePos());

		// Draw World
		olc::vi2d vTL = tv.GetTopLeftTile().max({ 0,0 });
		olc::vi2d vBR = tv.GetBottomRightTile().min(world.getMap().size());
		olc::vi2d vTile;
		for (vTile.y = vTL.y - 1; vTile.y <= vBR.y; vTile.y++)
			for (vTile.x = vTL.x - 1; vTile.x <= vBR.x; vTile.x++)
			{
				if (world.getMap().isBlock(vTile))
				{
					tv.DrawRect(vTile, { 1.0f, 1.0f }, olc::WHITE);
					tv.DrawLine(vTile, vTile + olc::vf2d(1.0f, 1.0f), olc::WHITE);
					tv.DrawLine(vTile + olc::vf2d(0.0f, 1.0f), vTile + olc::vf2d(1.0f, 0.0f), olc::WHITE);
				}
			}

		for (auto path = world.path_begin(); path != world.path_end(); ++path)
		{
			auto& p = (*path).second;
			if (p->empty()) continue;
			dw::Coord start = *(p->begin());
			for (auto iter = ++(p->begin()); iter != p->end(); ++iter)
			{
				Coord point = *iter;
				tv.DrawLine(start, point, olc::GREEN);
				start = point;
			}
		}

		// tv.FillRectDecal(vAreaTL, vAreaBR - vAreaTL + olc::vi2d(1, 1), olc::Pixel(0, 255, 255, 32));

		DrawString({ 40,40 }, "id = " + std::to_string((uint32_t)pFocusedAgent->getID()), olc::YELLOW);
		DrawString({ 40,50 }, " v = " + std::to_string(pFocusedAgent->getVelocity()), olc::YELLOW);
		DrawString({ 40,60 }, " a = (" + std::to_string(pFocusedAgent->getvAcceleration().x) + ", " + std::to_string(pFocusedAgent->getvAcceleration().y) + ")", olc::YELLOW);

		for (auto iter = world.agent_begin(); iter != world.agent_end(); ++iter) {
			auto& pAagent = (*iter).second;

			std::shared_ptr<dw::Agent> pAgent = (*iter).second;
			olc::Pixel AgentColor = (pAgent == pFocusedAgent ? olc::YELLOW : olc::WHITE);
			// Draw Agent
			tv.DrawCircle(pAgent->getPosition(), (float)pAgent->getRadius(), AgentColor);

			// Draw Name
			tv.DrawString(pAgent->getPosition() + pAgent->getRadius() * olc::vf2d(-1.0f, 1.0f), std::to_string((uint32_t)pAgent->getID()), AgentColor, olc::vf2d(1.0f, 1.0f));

			// Draw Velocity
			tv.DrawLine(pAgent->getPosition(), pAgent->getPosition() + pAgent->getDirection() * pAgent->getVelocity(), olc::RED);
			// Draw Direction
			tv.DrawLine(pAgent->getPosition(), pAgent->getPosition() + pAgent->getDirection() * pAgent->getRadius(), olc::MAGENTA);
		}

		return true;
	}
};

int main()
{
	DigitalWorldUI demo;
	if (demo.Construct(640, 480, 2, 2))
		demo.Start();
	return 0;
}